[AWS CDK] 一撃でCloudFrontとS3を使ったWebサイトを構築してみた
パッと静的Webサイトを用意したい
こんにちは、のんピ(@non____97)です。
皆さんはパッと静的Webサイトを用意したいなと思ったことはありますか? 私はあります。
AWS上で静的Webサイトを構築するとなると思いつくのは「CloudFront + S3」の構成です。しかし、OACの設定をしたりアクセスログの設定をしたりと意外と設定する項目が多く大変です。そのため、検証目的で用意する際には手間がかかります。
毎回都度用意するのも面倒なので、AWS CDKを使って一撃で構築できるようにしてみました。(Route 53 Public Hosted Zoneを作成する場合は二撃です)
AWS CDKのコードの紹介
やっていること
AWS CDKのコードは以下リポジトリに保存しています。
やっていることは以下のとおりです。
- Route 53 Public Hosted Zoneの作成 または インポート (Optional)
- ACM証明書の作成 または インポート (Optional)
- S3サーバーアクセスログ用S3バケットの作成 (Optional)
- CloudFrontのアクセスログ用S3バケットの作成 (Optional)
- Webサイトのコンテンツを保存するS3バケットの作成
- ディレクトリインデックス用のCloudFront Functionsの作成 (Optional)
- ディレクトリインデックス用のLambda@Edgeの作成 (Optional)
- CloudFront ディストリビューションの作成
- CloudFront OACの設定
- Route 53 Public Hosted ZoneにCloudFront ディストリビューションのALIASレコードを作成 (Optional)
- Webサイトのコンテンツを保存するS3バケットにコンテンツをアップロード (Optional)
ディレクトリインデックス用の仕組みはCloudFront FunctionsとLambda@Edgeの2種類を用意しました。CloudFront Functionsの方がシンプルではあるのですが、CloudFront Functionsのリクエストに対するコストが気になる人もいるかと思います。キャッシュのTTLが長いなどキャッシュヒット率も高いであれば、オリジンリクエストで実行が可能なLambda@Edgeの方がコストが安くなるケースがあります。詳細は以下記事をご覧ください。
「そもそもディレクトリインデックスとは?」という方は以下AWS Blogをご覧ください。
S3バケットへのコンテンツのアップロードはaws_s3_deployment.BucketDeploymentを使用しています。指定したディレクトリパスをzipで固めてからアップロードされます。指定したディレクトリ内のファイルを少しでも追加すると、全てのファイルがアップロードされ直されます。あまりに大量のコンテンツがある場合はエラーになるかもしれないので注意してください。
AWS WAFの設定はデプロイ後にお好みでどうぞ。今だとマネジメントコンソールから簡単にAWS WAFのWebACLの作成とディストリビューションへのアタッチができます。
CloudFrontディストリビューションの設定
CloudFrontディストリビューション周りの設定は以下のとおりです。
this.distribution = new cdk.aws_cloudfront.Distribution(this, "Default", { defaultRootObject: "index.html", errorResponses: [ { ttl: cdk.Duration.minutes(1), httpStatus: 403, responseHttpStatus: 403, responsePagePath: "/error.html", }, { ttl: cdk.Duration.minutes(1), httpStatus: 404, responseHttpStatus: 404, responsePagePath: "/error.html", }, ], defaultBehavior: { origin: new cdk.aws_cloudfront_origins.S3Origin( props.websiteBucketConstruct.bucket ), allowedMethods: cdk.aws_cloudfront.AllowedMethods.ALLOW_GET_HEAD, cachedMethods: cdk.aws_cloudfront.CachedMethods.CACHE_GET_HEAD, cachePolicy: cdk.aws_cloudfront.CachePolicy.CACHING_OPTIMIZED, viewerProtocolPolicy: cdk.aws_cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS, responseHeadersPolicy: cdk.aws_cloudfront.ResponseHeadersPolicy.SECURITY_HEADERS, functionAssociations: directoryIndexCF2 ? [ { function: directoryIndexCF2, eventType: cdk.aws_cloudfront.FunctionEventType.VIEWER_REQUEST, }, ] : undefined, edgeLambdas: directoryIndexLambdaEdge ? [ { functionVersion: directoryIndexLambdaEdge.currentVersion, eventType: cdk.aws_cloudfront.LambdaEdgeEventType.ORIGIN_REQUEST, }, ] : undefined, }, httpVersion: cdk.aws_cloudfront.HttpVersion.HTTP2_AND_3, priceClass: cdk.aws_cloudfront.PriceClass.PRICE_CLASS_ALL, domainNames: props.domainName ? [props.domainName] : undefined, certificate: props.domainName ? props.certificateConstruct?.certificate : undefined, logBucket: props.cloudFrontAccessLogBucketConstruct?.bucket, logFilePrefix: props.logFilePrefix, });
現在はキャッシュポリシーとレスポンスヘッダーポリシーのどちらもマネージドのものを使用しています。
キャッシュの設定を変更したい場合は、以下AWS公式ドキュメントを参考にしてキャッシュポリシーを作成ください。
また、レスポンスヘッダーからserver
を削除したい場合は、以下記事を参考に設定してください。
各種パラメーター
各種パラメーターの設定は以下ファイルで行います。
import * as cdk from "aws-cdk-lib"; import * as path from "path"; export interface LifecycleRule { prefix?: string; // ライフサイクルルールを適用するオブジェクトのプレフィックス expirationDays: number; // オブジェクトの保持期間 ruleNameSuffix?: string; // ライフサイクルルールに付与するサフィックス abortIncompleteMultipartUploadAfter?: cdk.Duration; // 不完全なマルチパートアップロードを削除するまでの期間 } export interface AccessLog { enableAccessLog?: boolean; // アクセスログを有効化するか logFilePrefix?: string; // 出力するアクセスログのプレフィックス lifecycleRules?: LifecycleRule[]; // 適用するライフサイクルルール } export interface HostZoneProperty { zoneName?: string; // Public Hosted Zoneのゾーン名 hostedZoneId?: string; // 既存のPublic Hosted ZoneのID } export interface CertificateProperty { certificateArn?: string; // 既存のACM証明書のID certificateDomainName?: string; // ACM証明書のドメイン名 } export interface ContentsDeliveryProperty { domainName?: string; // CloudFrontディストリビューションに設定するドメイン名 contentsPath?: string; // Webサイト用のS3バケットにPUTするコンテンツのローカルパス enableDirectoryIndex?: "cf2" | "lambdaEdge" | false; // ディレクトリインデックス機能の実装方法 enableS3ListBucket?: boolean; // CloudFrontディストリビューションからS3バケットに対して s3:ListBucket を許可するか 存在しないオブジェクトにアクセスした場合に404で返したい場合は有効化 } export interface WebsiteProperty { hostedZone?: HostZoneProperty; certificate?: CertificateProperty; contentsDelivery?: ContentsDeliveryProperty; allowDeleteBucketAndObjects?: boolean; // S3バケットの削除 および S3バケット内のオブジェクトを削除するか s3ServerAccessLog?: AccessLog; cloudFrontAccessLog?: AccessLog; } export interface WebsiteStackProperty { env?: cdk.Environment; props: WebsiteProperty; } export const websiteStackProperty: WebsiteStackProperty = { env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION, }, props: { hostedZone: { zoneName: "www.non-97.net", }, certificate: { certificateDomainName: "www.non-97.net", }, contentsDelivery: { domainName: "www.non-97.net", contentsPath: path.join(__dirname, "../lib/src/contents"), enableDirectoryIndex: "cf2", enableS3ListBucket: true, }, allowDeleteBucketAndObjects: true, s3ServerAccessLog: { enableAccessLog: true, lifecycleRules: [{ expirationDays: 365 }], }, cloudFrontAccessLog: { enableAccessLog: true, lifecycleRules: [{ expirationDays: 365 }], }, }, };
デプロイ (Ver. CloudFront Functions)
デプロイ
実際にデプロイして試してみます。
設定は以下のようにしています。
export const websiteStackProperty: WebsiteStackProperty = { env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION, }, props: { hostedZone: { zoneName: "www.non-97.net", }, certificate: { certificateDomainName: "www.non-97.net", }, contentsDelivery: { domainName: "www.non-97.net", contentsPath: path.join(__dirname, "../lib/src/contents"), enableDirectoryIndex: "cf2", enableS3ListBucket: true, }, allowDeleteBucketAndObjects: true, s3ServerAccessLog: { enableAccessLog: true, lifecycleRules: [{ expirationDays: 365 }], }, cloudFrontAccessLog: { enableAccessLog: true, lifecycleRules: [{ expirationDays: 365 }], }, }, };
デプロイが走ると、Route 53 Public Hosted Zoneが作成されます。NSレコードを上位のゾーン(私の場合はnon-97.net
)に登録しましょう。登録が完了してしばらくすると、ACMでの証明書の発行など後続の処理が完了します。
全体で8分ほどでデプロイが完了しました。
動作確認
実際にアクセスしてみましょう。
$ curl https://www.non-97.net -IL HTTP/2 200 content-type: text/html content-length: 12 date: Thu, 28 Mar 2024 07:07:52 GMT last-modified: Thu, 28 Mar 2024 06:45:06 GMT etag: "56aec8b7843df637b3fb2ec0b027e5b6" x-amz-server-side-encryption: AES256 accept-ranges: bytes server: AmazonS3 x-cache: Miss from cloudfront via: 1.1 d1fa9409a9380374423ca786990631ba.cloudfront.net (CloudFront) x-amz-cf-pop: NRT57-P2 alt-svc: h3=":443"; ma=86400 x-amz-cf-id: oPZk8a0JVb2HagQLZyIjmmYNEql7pU7Dt6pd6fPbZ9BVZEOtBTSB7Q== x-xss-protection: 1; mode=block x-frame-options: SAMEORIGIN referrer-policy: strict-origin-when-cross-origin x-content-type-options: nosniff strict-transport-security: max-age=31536000 $ curl https://www.non-97.net -IL HTTP/2 200 content-type: text/html content-length: 12 date: Thu, 28 Mar 2024 07:07:52 GMT last-modified: Thu, 28 Mar 2024 06:45:06 GMT etag: "56aec8b7843df637b3fb2ec0b027e5b6" x-amz-server-side-encryption: AES256 accept-ranges: bytes server: AmazonS3 x-cache: Hit from cloudfront via: 1.1 180bb14f3969a5383ec3b52ad1ce5ad6.cloudfront.net (CloudFront) x-amz-cf-pop: NRT57-P2 alt-svc: h3=":443"; ma=86400 x-amz-cf-id: mOimDjaGRT4Jsml8gthlfSl2TCYuO1qYFSKR-XNbBdqizL_2HDytWQ== age: 9 x-xss-protection: 1; mode=block x-frame-options: SAMEORIGIN referrer-policy: strict-origin-when-cross-origin x-content-type-options: nosniff strict-transport-security: max-age=31536000 $ curl https://www.non-97.net/index.html -IL HTTP/2 200 content-type: text/html content-length: 12 date: Thu, 28 Mar 2024 07:07:52 GMT last-modified: Thu, 28 Mar 2024 06:45:06 GMT etag: "56aec8b7843df637b3fb2ec0b027e5b6" x-amz-server-side-encryption: AES256 accept-ranges: bytes server: AmazonS3 x-cache: Hit from cloudfront via: 1.1 6a4098eaf995c1e965d6434534971664.cloudfront.net (CloudFront) x-amz-cf-pop: NRT57-P2 alt-svc: h3=":443"; ma=86400 x-amz-cf-id: Aer0EiYUTtwi0u3cFT2qXVAUc18N22GIS0YornuBzbjvjmHGmU41cg== age: 16 x-xss-protection: 1; mode=block x-frame-options: SAMEORIGIN referrer-policy: strict-origin-when-cross-origin x-content-type-options: nosniff strict-transport-security: max-age=31536000
CloudFrontのキャッシュが効いていそうです。HSTSのヘッダーも追加されていますね。
HTTPでアクセスした場合にHTTPSにリダイレクトするようにもしています。
$ curl http://www.non-97.net/test.html -IL HTTP/1.1 301 Moved Permanently Server: CloudFront Date: Thu, 28 Mar 2024 07:10:14 GMT Content-Type: text/html Content-Length: 167 Connection: close Location: https://www.non-97.net/test.html X-Cache: Redirect from cloudfront Via: 1.1 b93822242d240fe957b16155421ce866.cloudfront.net (CloudFront) X-Amz-Cf-Pop: NRT57-P2 Alt-Svc: h3=":443"; ma=86400 X-Amz-Cf-Id: UuGHMYX2UCWQJswF-E7YwhQMWiHjHmGZBAeehOv3EsP7xdDklBuUWw== X-XSS-Protection: 1; mode=block X-Frame-Options: SAMEORIGIN Referrer-Policy: strict-origin-when-cross-origin X-Content-Type-Options: nosniff HTTP/2 200 content-type: text/html content-length: 11 date: Thu, 28 Mar 2024 07:10:15 GMT last-modified: Thu, 28 Mar 2024 06:45:06 GMT etag: "aa83444f341b53601faa67868d57abd6" x-amz-server-side-encryption: AES256 accept-ranges: bytes server: AmazonS3 x-cache: Miss from cloudfront via: 1.1 aaaa38f6638fefc2221f20ff18eceef2.cloudfront.net (CloudFront) x-amz-cf-pop: NRT57-P2 alt-svc: h3=":443"; ma=86400 x-amz-cf-id: gBa6aohc9hBViovBo_1EDDcYEJH57q_TqALrOG0AuHn1D4pnSsb95A== x-xss-protection: 1; mode=block x-frame-options: SAMEORIGIN referrer-policy: strict-origin-when-cross-origin x-content-type-options: nosniff strict-transport-security: max-age=31536000
ディレクトリインデックスが動作していることも確認しておきます。
$ curl https://www.non-97.net/dir -IL HTTP/2 200 content-type: text/html content-length: 16 date: Thu, 28 Mar 2024 07:10:48 GMT last-modified: Thu, 28 Mar 2024 06:45:05 GMT etag: "64f1d28c08f68bb7a25dd16598eed1d2" x-amz-server-side-encryption: AES256 accept-ranges: bytes server: AmazonS3 x-cache: Miss from cloudfront via: 1.1 c9203ba15af2ae82294719bd8bb5fcce.cloudfront.net (CloudFront) x-amz-cf-pop: NRT57-P2 alt-svc: h3=":443"; ma=86400 x-amz-cf-id: j0a9BgZiU2SAop_jWqmixYDUQPqihmz8_GFL5JvZPqIN6utiyBOlyg== x-xss-protection: 1; mode=block x-frame-options: SAMEORIGIN referrer-policy: strict-origin-when-cross-origin x-content-type-options: nosniff strict-transport-security: max-age=31536000 $ curl https://www.non-97.net/dir/ -IL HTTP/2 200 content-type: text/html content-length: 16 date: Thu, 28 Mar 2024 07:10:48 GMT last-modified: Thu, 28 Mar 2024 06:45:05 GMT etag: "64f1d28c08f68bb7a25dd16598eed1d2" x-amz-server-side-encryption: AES256 accept-ranges: bytes server: AmazonS3 x-cache: Hit from cloudfront via: 1.1 3bc9fc5ff5b1c7e58ac789581c13d0e4.cloudfront.net (CloudFront) x-amz-cf-pop: NRT57-P2 alt-svc: h3=":443"; ma=86400 x-amz-cf-id: rhoeQsW7atFnVL4Az4xIxkkIFNg3AMj6piy725hSqCc7D6RXOm4ZfA== age: 7 x-xss-protection: 1; mode=block x-frame-options: SAMEORIGIN referrer-policy: strict-origin-when-cross-origin x-content-type-options: nosniff strict-transport-security: max-age=31536000
問題なく動作していますね。
CloudFrontアクセスログ、S3サーバーアクセスログも問題なく出力されています。
CloudFrontアクセスログ
サーバーアクセスログ
大量アクセス時のCloudFront Functionsの挙動確認
試しにApache Benchで大量にアクセスしてみます。事前にキャッシュは削除しておきます。
$ ab -n 10000 -c 100 https://www.non-97.net/dir/ This is ApacheBench, Version 2.3 <$Revision: 1903618 $> Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ Licensed to The Apache Software Foundation, http://www.apache.org/ Benchmarking www.non-97.net (be patient) Completed 1000 requests Completed 2000 requests Completed 3000 requests Completed 4000 requests Completed 5000 requests Completed 6000 requests Completed 7000 requests Completed 8000 requests Completed 9000 requests Completed 10000 requests Finished 10000 requests Server Software: AmazonS3 Server Hostname: www.non-97.net Server Port: 443 SSL/TLS Protocol: TLSv1.2,ECDHE-RSA-AES128-GCM-SHA256,2048,128 Server Temp Key: ECDH X25519 253 bits TLS Server Name: www.non-97.net Document Path: /dir/ Document Length: 16 bytes Concurrency Level: 100 Time taken for tests: 36.458 seconds Complete requests: 10000 Failed requests: 0 Total transferred: 7267060 bytes HTML transferred: 160000 bytes Requests per second: 274.29 [#/sec] (mean) Time per request: 364.582 [ms] (mean) Time per request: 3.646 [ms] (mean, across all concurrent requests) Transfer rate: 194.65 [Kbytes/sec] received Connection Times (ms) min mean[+/-sd] median max Connect: 56 298 110.3 296 1132 Processing: 9 54 64.3 29 1008 Waiting: 8 32 33.6 23 1000 Total: 71 352 131.1 333 1225 Percentage of the requests served within a certain time (ms) 50% 333 66% 359 75% 383 80% 403 90% 487 95% 563 98% 693 99% 872 100% 1225 (longest request)
CloudFront Functionsのメトリクスを確認すると、1万回実行されていました。
また、Compute Utilizationも一時的に跳ねていました。
デプロイ (Ver. Lambda@Edge)
デプロイ
次にディレクトリインデックスの機能をLambda@Edgeで動かしてみましょう。
export const websiteStackProperty: WebsiteStackProperty = { env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION, }, props: { hostedZone: { zoneName: "www.non-97.net", }, certificate: { certificateDomainName: "www.non-97.net", }, contentsDelivery: { domainName: "www.non-97.net", contentsPath: path.join(__dirname, "../lib/src/contents"), enableDirectoryIndex: "lambdaEdge", enableS3ListBucket: true, }, allowDeleteBucketAndObjects: true, s3ServerAccessLog: { enableAccessLog: true, lifecycleRules: [{ expirationDays: 365 }], }, cloudFrontAccessLog: { enableAccessLog: true, lifecycleRules: [{ expirationDays: 365 }], }, }, };
npx cdk diff
の結果は以下のとおりです。
$ npx cdk diff Bundling asset WebsiteStack/ContentsDeliveryConstruct/DirectoryIndexLambdaEdge/Code/Stage... cdk.out/bundling-temp-a88b3b0269470979bc0b8d1f8ed8a4f028c4b7f1fe42067392775b7b09619397/index.mjs 163b ⚡ Done in 6ms Stack WebsiteStack Hold on while we create a read-only change set to get a diff with accurate replacement information (use --no-change-set to use a less accurate but faster template-only diff) IAM Statement Changes ┌───┬──────────────────────────────────────────────────────────┬────────┬────────────────┬──────────────────────────────────────────────────────────────┬───────────┐ │ │ Resource │ Effect │ Action │ Principal │ Condition │ ├───┼──────────────────────────────────────────────────────────┼────────┼────────────────┼──────────────────────────────────────────────────────────────┼───────────┤ │ + │ ${ContentsDeliveryConstruct/LambdaEdgeExecutionRole.Arn} │ Allow │ sts:AssumeRole │ Service:edgelambda.amazonaws.com │ │ │ │ │ │ │ Service:lambda.amazonaws.com │ │ └───┴──────────────────────────────────────────────────────────┴────────┴────────────────┴──────────────────────────────────────────────────────────────┴───────────┘ IAM Policy Changes ┌───┬──────────────────────────────────────────────────────┬────────────────────────────────────────────────────────────────────────────────┐ │ │ Resource │ Managed Policy ARN │ ├───┼──────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────┤ │ + │ ${ContentsDeliveryConstruct/LambdaEdgeExecutionRole} │ arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole │ └───┴──────────────────────────────────────────────────────┴────────────────────────────────────────────────────────────────────────────────┘ (NOTE: There may be security-related changes not in this list. See https://github.com/aws/aws-cdk/issues/1299) Resources [-] AWS::CloudFront::Function ContentsDeliveryConstruct/DirectoryIndexCF2 ContentsDeliveryConstructDirectoryIndexCF2F9EC47B1 destroy [+] AWS::IAM::Role ContentsDeliveryConstruct/LambdaEdgeExecutionRole ContentsDeliveryConstructLambdaEdgeExecutionRoleF34170E4 [+] AWS::Lambda::Function ContentsDeliveryConstruct/DirectoryIndexLambdaEdge ContentsDeliveryConstructDirectoryIndexLambdaEdgeDD789DA4 [+] AWS::Lambda::Version ContentsDeliveryConstruct/DirectoryIndexLambdaEdge/CurrentVersion ContentsDeliveryConstructDirectoryIndexLambdaEdgeCurrentVersion93C358E81c9380644828904ade854fd138db7b43 [~] AWS::CloudFront::Distribution ContentsDeliveryConstruct/Default ContentsDeliveryConstructE854BE87 └─ [~] DistributionConfig └─ [~] .DefaultCacheBehavior: ├─ [-] Removed: .FunctionAssociations └─ [+] Added: .LambdaFunctionAssociations ✨ Number of stacks with differences: 1
npx cdk deploy
でデプロイします。デプロイは3分ほどで完了しました。
動作確認
ディレクトリインデックスが効いているか確認します。
$ curl https://www.non-97.net/dir/ -IL HTTP/2 200 content-type: text/html content-length: 16 date: Thu, 28 Mar 2024 07:52:14 GMT last-modified: Thu, 28 Mar 2024 06:45:05 GMT etag: "64f1d28c08f68bb7a25dd16598eed1d2" x-amz-server-side-encryption: AES256 accept-ranges: bytes server: AmazonS3 x-cache: Miss from cloudfront via: 1.1 49b964f897a5e1c9f9d0e182630ef7ca.cloudfront.net (CloudFront) x-amz-cf-pop: NRT57-P2 alt-svc: h3=":443"; ma=86400 x-amz-cf-id: JntntZB9DIsrt0RJQkND0oCneZhrAN3B36Wk-u_dUvWl0OeyTdN4tg== x-xss-protection: 1; mode=block x-frame-options: SAMEORIGIN referrer-policy: strict-origin-when-cross-origin x-content-type-options: nosniff strict-transport-security: max-age=31536000 $ curl https://www.non-97.net/dir/ -IL HTTP/2 200 content-type: text/html content-length: 16 date: Thu, 28 Mar 2024 07:52:14 GMT last-modified: Thu, 28 Mar 2024 06:45:05 GMT etag: "64f1d28c08f68bb7a25dd16598eed1d2" x-amz-server-side-encryption: AES256 accept-ranges: bytes server: AmazonS3 x-cache: Hit from cloudfront via: 1.1 b93822242d240fe957b16155421ce866.cloudfront.net (CloudFront) x-amz-cf-pop: NRT57-P2 alt-svc: h3=":443"; ma=86400 x-amz-cf-id: Ks4bMLN58XaZ_wp4pq2pp_7BZBBvno71I0k3u9iknrE67mfbmoJVVw== age: 12 x-xss-protection: 1; mode=block x-frame-options: SAMEORIGIN referrer-policy: strict-origin-when-cross-origin x-content-type-options: nosniff strict-transport-security: max-age=31536000
効いていますね。
大量アクセス時のLambda@Edgeの挙動確認
Lambda@EdgeでもApache Benchで10,000回アクセスしてみます。
ab -n 10000 -c 100 https://www.non-97.net/dir/ This is ApacheBench, Version 2.3 <$Revision: 1903618 $> Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ Licensed to The Apache Software Foundation, http://www.apache.org/ Benchmarking www.non-97.net (be patient) Completed 1000 requests Completed 2000 requests Completed 3000 requests Completed 4000 requests Completed 5000 requests Completed 6000 requests Completed 7000 requests Completed 8000 requests Completed 9000 requests Completed 10000 requests Finished 10000 requests Server Software: AmazonS3 Server Hostname: www.non-97.net Server Port: 443 SSL/TLS Protocol: TLSv1.2,ECDHE-RSA-AES128-GCM-SHA256,2048,128 Server Temp Key: ECDH X25519 253 bits TLS Server Name: www.non-97.net Document Path: /dir/ Document Length: 16 bytes Concurrency Level: 100 Time taken for tests: 35.434 seconds Complete requests: 10000 Failed requests: 0 Total transferred: 7266592 bytes HTML transferred: 160000 bytes Requests per second: 282.22 [#/sec] (mean) Time per request: 354.338 [ms] (mean) Time per request: 3.543 [ms] (mean, across all concurrent requests) Transfer rate: 200.27 [Kbytes/sec] received Connection Times (ms) min mean[+/-sd] median max Connect: 47 289 85.3 293 646 Processing: 8 52 45.6 37 1017 Waiting: 7 34 28.3 25 1010 Total: 79 342 102.2 339 1119 Percentage of the requests served within a certain time (ms) 50% 339 66% 373 75% 398 80% 417 90% 464 95% 528 98% 594 99% 636 100% 1119 (longest request)
CloudWatchメトリクスでLambda@Edgeが呼びされた回数を見ると、該当の時間に東京リージョンで呼び出された回数は1回だけでした。
Lambda@EdgeがCloudWatch Logsに出力したログを見ても、1回しか実行されていないことが分かります。キャッシュヒット率が高い場合はLambda@Edgeの方がコストが安くなるかもしれませんね。
キャッシュヒットされていることも確認しましょう。分かりづらいですが、CloudFront Functionsの場合もLambda@Edgeの場合もどちらもほぼ100%キャッシュヒットしていることが分かります。キャッシュヒットしていないのは最初のアクセスのみです。
用途に応じてCloudFront Functionsか、Lambda@Edgeを使うか判断しましょう。使い分けは以下記事が参考になります。
なお、Lambda@Edgeの関数を削除する際には、以下のように失敗を繰り返します。(最終的には正常に削除される)
これは以下記事でも紹介されているとおり、Lambdaがレプリカを持っているためです。
補足 : L2 ConstructでS3をオリジンに設定すると、問答無用でOAIが作成される
デプロイ後に気になる方がいるかもしれないので補足です。
CloudFrontとS3バケット間のアクセス制御はOACで行っています。OACの説明は以下記事をご覧ください。
OACを使うため、OAIは使用しません。AWS CDK上でもOAIの設定はしていません。しかし、OAIは自動で作成されます。
このOAIはCloudFormationのコンソールからコンストラクトツリーを表示すると、CloudFrontディストリビューションDefault
の子コンストラクトであることが分かります。
「じゃあthis.distribution.node.tryRemoveChild("Origin1");
で、このコンストラクトを削除すれば良いじゃん!」と思われるかもしれません。しかし、これはできません。やろうとすると、以下のように怒られます。
WebsiteStack: creating CloudFormation changeset... ❌ WebsiteStack failed: Error [ValidationError]: Template error: instance of Fn::GetAtt references undefined resource ContentsDeliveryConstructOrigin1S3Origin9C471993 at Request.extractError (/<ディレクトリパス>/node_modules/aws-cdk/lib/index.js:382:46692) at Request.callListeners (/<ディレクトリパス>/node_modules/aws-cdk/lib/index.js:382:91452) at Request.emit (/<ディレクトリパス>/node_modules/aws-cdk/lib/index.js:382:90900) at Request.emit (/<ディレクトリパス>/node_modules/aws-cdk/lib/index.js:382:199296) at Request.transition (/<ディレクトリパス>/node_modules/aws-cdk/lib/index.js:382:192848) at AcceptorStateMachine.runTo (/<ディレクトリパス>/node_modules/aws-cdk/lib/index.js:382:157720) at /<ディレクトリパス>/node_modules/aws-cdk/lib/index.js:382:158050 at Request.<anonymous> (/<ディレクトリパス>/node_modules/aws-cdk/lib/index.js:382:193140) at Request.<anonymous> (/<ディレクトリパス>/node_modules/aws-cdk/lib/index.js:382:199371) at Request.callListeners (/<ディレクトリパス>/node_modules/aws-cdk/lib/index.js:382:91620) { code: 'ValidationError', time: 2024-03-27T11:24:11.495Z, requestId: '6761120e-8a9b-4871-b0a0-7ee1cd9e5545', statusCode: 400, retryable: false, retryDelay: 60.263640837447284 }
これは自動で作成されたOAIを参照して、オリジンのS3バケットのバケットポリシーを設定しているためです。
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Deny", "Principal": { "AWS": "*" }, "Action": "s3:*", "Resource": [ "arn:aws:s3:::websitestack-websitebucketconstruct04d5d64f-6jt680fmu9eu", "arn:aws:s3:::websitestack-websitebucketconstruct04d5d64f-6jt680fmu9eu/*" ], "Condition": { "Bool": { "aws:SecureTransport": "false" } } }, { "Effect": "Allow", "Principal": { "AWS": "arn:aws:iam::<AWSアカウントID>:role/WebsiteStack-CustomS3AutoDeleteObjectsCustomResourc-nHlT8dYG9tuj" }, "Action": [ "s3:DeleteObject*", "s3:GetBucket*", "s3:List*", "s3:PutBucketPolicy" ], "Resource": [ "arn:aws:s3:::websitestack-websitebucketconstruct04d5d64f-6jt680fmu9eu", "arn:aws:s3:::websitestack-websitebucketconstruct04d5d64f-6jt680fmu9eu/*" ] }, { "Effect": "Allow", "Principal": { "AWS": "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity E243GPZLTPBOD" }, "Action": "s3:GetObject", "Resource": "arn:aws:s3:::websitestack-websitebucketconstruct04d5d64f-6jt680fmu9eu/*" }, { "Effect": "Allow", "Principal": { "Service": "cloudfront.amazonaws.com" }, "Action": [ "s3:GetObject", "s3:ListBucket" ], "Resource": [ "arn:aws:s3:::websitestack-websitebucketconstruct04d5d64f-6jt680fmu9eu", "arn:aws:s3:::websitestack-websitebucketconstruct04d5d64f-6jt680fmu9eu/*" ], "Condition": { "StringEquals": { "AWS:SourceArn": "arn:aws:cloudfront::<AWSアカウントID>:distribution/E8PZWTYQZP0PV" } } } ] }
GitHubのソースコードaws-cdk/packages/aws-cdk-lib/aws-cloudfront-origins/lib/s3-origin.tsを確認すると、addToResourcePolicy()
でポリシーを追加していることが分かります。
/** * An Origin specific to a S3 bucket (not configured for website hosting). * * Contains additional logic around bucket permissions and origin access identities. */ class S3BucketOrigin extends cloudfront.OriginBase { private originAccessIdentity!: cloudfront.IOriginAccessIdentity; constructor(private readonly bucket: s3.IBucket, { originAccessIdentity, ...props }: S3OriginProps) { super(bucket.bucketRegionalDomainName, props); if (originAccessIdentity) { this.originAccessIdentity = originAccessIdentity; } } public bind(scope: Construct, options: cloudfront.OriginBindOptions): cloudfront.OriginBindConfig { if (!this.originAccessIdentity) { // Using a bucket from another stack creates a cyclic reference with // the bucket taking a dependency on the generated S3CanonicalUserId for the grant principal, // and the distribution having a dependency on the bucket's domain name. // Fix this by parenting the OAI in the bucket's stack when cross-stack usage is detected. const bucketStack = cdk.Stack.of(this.bucket); const bucketInDifferentStack = bucketStack !== cdk.Stack.of(scope); const oaiScope = bucketInDifferentStack ? bucketStack : scope; const oaiId = bucketInDifferentStack ? `${cdk.Names.uniqueId(scope)}S3Origin` : 'S3Origin'; this.originAccessIdentity = new cloudfront.OriginAccessIdentity(oaiScope, oaiId, { comment: `Identity for ${options.originId}`, }); } // Used rather than `grantRead` because `grantRead` will grant overly-permissive policies. // Only GetObject is needed to retrieve objects for the distribution. // This also excludes KMS permissions; currently, OAI only supports SSE-S3 for buckets. // Source: https://aws.amazon.com/blogs/networking-and-content-delivery/serving-sse-kms-encrypted-content-from-s3-using-cloudfront/ this.bucket.addToResourcePolicy(new iam.PolicyStatement({ resources: [this.bucket.arnForObjects('*')], actions: ['s3:GetObject'], principals: [this.originAccessIdentity.grantPrincipal], })); return super.bind(scope, options); } protected renderS3OriginConfig(): cloudfront.CfnDistribution.S3OriginConfigProperty | undefined { return { originAccessIdentity: `origin-access-identity/cloudfront/${this.originAccessIdentity.originAccessIdentityId}` }; } }
AWS CDKでは指定したポリシーステートメントをピンポイントで削除する処理はできない認識です。そのため、OAIは削除せずにそのままにしています。
コスト試算
この環境のコストを試算してみましょう。
条件は以下のとおりです。
- コンテンツ量 : 20GB
- ログ出力量 : 1TB
- アクセス数 : 10,000,000回/month
- 1アクセス当たりの平均転送量 : 1MB
- 転送量 : 20TB/month
- キャッシュヒット率 : 90%
- Public Hosted Zone : 1つ
- ディレクトリインデックス : CloudFront Functionsで実装
試算結果は以下のとおりです。
- S3
- データサイズ料金 :
26.10 USD
- GETリクエスト料金 :
0.37 USD
- データサイズ料金 :
- CloudFrontの
- インターネットへのデータ転送料金 :
2,078.72 USD
- オリジンへのデータ転送料金 :
122.88 USD
- HTTPSリクエスト料金 :
12.00 USD
- CloudFront Functions実行料金 :
1.00 USD
- インターネットへのデータ転送料金 :
- Route 53 Public Hosted Zone
- Hosted Zone料金 :
0.5 USD
- Hosted Zone料金 :
- トータル料金
2,240.57 USD/month (= 336,086円)
※ 1ドル150円
それなりです。
こんな時にありがたいのがクラスメソッドメンバーズのEC2・CDN割引プランです。
クラスメソッドメンバーズのEC2・CDN割引プランを使うと、なんとCloudFrontのアウトバウンド通信費が従来$0.114/GBのところ$0.0456/GBと、60%オフの料金で使用できたり、GETリクエストの料金が無料になるなどの割引があります。
抜粋 : AWS請求代行・請求書払い(リセール) | クラスメソッド株式会社
これにより、トータルの料金はから、2,240.57 USD/month (= 336,086円)
から870.96 USD/month (= 130,644円)
と毎月約20万円のコスト削減になります。やったぜ。
CloudFrontとS3を使った静的Webサイトが欲しい時に
AWS CDKを使って一撃でCloudFrontとS3を使ったWebサイトを構築してみました。
CloudFrontとS3を使った静的Webサイトが欲しい時にご利用ください。上述のコードをベースにキャッシュポリシーをカスタムしたり、ログ分析用のAthenaを追加したり、コンテンツデプロイのCI/CDパイプラインを作っても良いと思います。
この記事が誰かの助けになれば幸いです。
以上、AWS事業本部 コンサルティング部の のんピ(@non____97)でした!